﻿//------------------------------------------------------------------------------
// <copyright file="ModelingAddin.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

using System;
using System.Collections;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.ComponentModel.Design;
using EnvDTE;
using EnvDTE80;
using Extensibility;
using Microsoft.VisualStudio.Shell;
using VsOle = Microsoft.VisualStudio.OLE.Interop;
using VsShell = Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Modeling.Shell;
using System.Reflection;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;

namespace ModelingAddIns
{
    /// <summary>
    /// Common base class for all modeling addins.
    /// </summary>
    /// <remarks>
    /// This class is ComVisible in order to addin classes which inherit from here to be functional.
    /// </remarks>
    [ComVisible(true)]
    public abstract class ModelingAddIn : IDisposable, IDTExtensibility2, IDTCommandTarget, ITypedServiceProvider
    {
        #region Private variables, properties

        private DTE2 m_applicationObject;
        private AddIn m_addInInstance;
        private ServiceProvider m_serviceProvider;
        private MetaFeatureStore m_store;
        private Hashtable m_services;
        private GlobalContext m_globalContext;
        private static Dictionary<Assembly, ModelingAddIn> s_addins;

        /// <summary>
        /// Gets the DTE application object.
        /// </summary>
        public DTE2 ApplicationObject
        {
            [DebuggerStepThrough]
            get
            {
                Debug.Assert(m_applicationObject != null);
                return m_applicationObject;
            }
        }

        /// <summary>
        /// Gets the AddIn instance.
        /// </summary>
        public AddIn AddInInstance
        {
            [DebuggerStepThrough]
            get
            {
                Debug.Assert(m_addInInstance != null);
                return m_addInInstance;
            }
        }

        /// <summary>
        /// Gets VS Shell's service provider.
        /// </summary>
        private IServiceProvider ServiceProvider
        {
            [DebuggerStepThrough]
            get
            {
                Debug.Assert(m_serviceProvider != null);
                return m_serviceProvider;
            }
        }

        /// <summary>
        /// Gets MetaFeature store containing all meta-features loaded by this addin.
        /// </summary>
        public IMetaFeatureService Store
        {
            [DebuggerStepThrough]
            get { return m_store; }
        }

        #endregion

        #region IDisposable implementation

        ~ModelingAddIn()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            Debug.Assert(!disposing, "Addin class is not supposed to be explicitly disposed, but via OnDisconnection call.");
            if (disposing)
            {
                // Just ask the regular Close method to do its work if we're in a good state.
                Close();
            }
            // We don't hold on to any unmanaged resources, neither we can rely on GC state
            // to do the proper cleanup on managed stuff, so just release roots fields here.
            m_services = null;
            m_store = null;
            m_globalContext = null;
        }

        #endregion

        #region Debug logging

        [Conditional("DEBUG")]
        private void DebugLog(string message)
        {
            if (m_addInInstance != null)
                Debug.WriteLine("[ " + this.AddInInstance.ProgID + " ] " + message);
            else
                Debug.WriteLine("[ ModelingAddIn ] " + message);
        }

        #endregion

        #region Initialization and cleanup

        private void Initialize()
        {
            m_serviceProvider = new ServiceProvider(this.ApplicationObject as VsOle.IServiceProvider);

            DebugLog("Initialize: Creating features.");
            m_store = new MetaFeatureStore(this);
            m_store.AddMetaFeature(typeof(GlobalContext));
            this.CreateMetaFeatures();

            DebugLog("Initialize: Installing features.");
            m_store.Install();

            DebugLog("Initialize: Loading features.");
            m_globalContext = m_store.FindMetaFeature(typeof(GlobalContext)).CreateFeature(this) as GlobalContext;

            if (s_addins == null)
                s_addins = new Dictionary<Assembly, ModelingAddIn>();
            Debug.Assert(!s_addins.ContainsKey(this.GetType().Assembly));
            s_addins[this.GetType().Assembly] = this;
        }

        /// <summary>
        /// Gets an instance of the add-in defined in given assembly.
        /// </summary>
        /// <param name="assembly">Add-in assembly.</param>
        /// <returns>Addin instance if add-in has been initialized.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1407:AvoidStaticMembersInComVisibleTypes")]
        public static ModelingAddIn GetInstance(Assembly assembly)
        {
            if (assembly == null) throw new ArgumentNullException("assembly");

            ModelingAddIn addin;
            if (s_addins != null && s_addins.TryGetValue(assembly, out addin))
                return addin;
            else
                return null;
        }

        protected void CreateDesignerContext()
        {
            this.Store.AddMetaFeature(typeof(DesignerContextProvider));
            this.Store.AddMetaFeature(typeof(DesignerContext));
            this.Store.AddMetaFeature(typeof(DiagramContext));
        }

        protected virtual void CreateMetaFeatures()
        {
            CreateDesignerContext();
            this.Store.AddMetaFeatures(this.GetType().Assembly, null);
        }

        private void Close()
        {
            if (m_globalContext != null)
            {
                DebugLog("Close: Unloading features.");
                m_globalContext.Dispose();
                m_globalContext = null;
            }

            if (m_store != null)
            {
                DebugLog("Close: Uninstalling meta-features.");
                m_store.Uninstall();

                DebugLog("Close: Disposing meta-features.");
                m_store.Dispose();
                m_store = null;
            }

            if (m_services != null)
            {
                DebugLog("Close: Disposing services.");
                foreach (object service in m_services.Values)
                {
                    IDisposable disposableService = service as IDisposable;
                    if (disposableService != null)
                        disposableService.Dispose();
                }
                m_services = null;
            }

            DebugLog("Close: Disposing other state.");

            if (m_serviceProvider != null)
            {
                m_serviceProvider.Dispose();
                m_serviceProvider = null;
            }

            Debug.Assert(s_addins != null);
            if (s_addins != null)
                s_addins.Remove(this.GetType().Assembly);
        }

        internal void Reload()
        {
            Debug.Assert(m_globalContext != null);
            if (m_globalContext != null)
            {
                m_globalContext.Dispose();
                m_globalContext = Store.FindMetaFeature(typeof(GlobalContext)).CreateFeature(this) as GlobalContext;
            }
        }

        #endregion

        #region IDTExtensibility2 Members

        [SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration")]
        public virtual void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
        {
            DebugLog("OnConnection: " + connectMode + ", initializing the addin.");

            if (application == null) throw new ArgumentNullException("application");
            if (addInInst == null) throw new ArgumentNullException("addInInst");

            m_applicationObject = (DTE2)application;
            m_addInInstance = (AddIn)addInInst;

            Initialize();

        }

        [SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration")]
        public virtual void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
        {
            DebugLog("OnDisconnection: " + removeMode + ", finalizing the addin.");
            Close();
        }

        public virtual void OnStartupComplete(ref Array custom)
        {
            DebugLog("OnStartupComplete");
        }

        public virtual void OnBeginShutdown(ref Array custom)
        {
            DebugLog("OnBeginShutdown");
        }

        public virtual void OnAddInsUpdate(ref Array custom)
        {
            DebugLog("OnAddInsUpdate");
        }

        #endregion

        #region IDTCommandTarget Members

        [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate")]
        [SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration")]
        public virtual void Exec(string commandName, vsCommandExecOption executeOption, ref object variantIn, ref object variantOut, ref bool handled)
        {
            DebugLog("Executing command " + commandName);
            INamedCommandService commandService = GetService<INamedCommandService>();
            Debug.Assert(commandService != null);
            handled = commandService != null && commandService.GlobalInvoke(commandName);
        }

        [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate")]
        [SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration")]
        public virtual void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus statusOption, ref object commandText)
        {
            INamedCommandService commandService = GetService<INamedCommandService>();
            Debug.Assert(commandService != null);
            if (commandService != null)
            {
                MenuCommand command = commandService.FindCommand(commandName);
                if (command != null)
                {
                    statusOption = (vsCommandStatus)command.OleStatus;
                    return;
                }
            }
            DebugLog("IDTCommandTarget.QueryStatus: called to provide status on unloaded command '" + commandName + "'.");
            statusOption = vsCommandStatus.vsCommandStatusUnsupported | vsCommandStatus.vsCommandStatusInvisible;
        }

        #endregion

        #region IServiceProvider Members

        /// <summary>
        /// Gets service of specified type.
        /// </summary>
        /// <param name="serviceType">The type of the service to retreive.</param>
        /// <returns>Retrieved service object or null if not supported.</returns>
        public object GetService(Type serviceType)
        {
            if (serviceType == null)
                throw new ArgumentNullException("serviceType");

            if (serviceType == typeof(IServiceProvider))
                return this;

            object service = null;
            if (m_services != null)
                service = m_services[serviceType];

            if (service == null)
            {
                if (m_services == null)
                    m_services = new Hashtable();

                if (serviceType == typeof(IMonitorSelectionService))
                {
                    DebugLog("Creating MonitorSelectionService");
                    Type type = typeof(ModelingPackage).Assembly.GetType("Microsoft.VisualStudio.EnterpriseTools.Shell.MonitorSelectionService");
                    service = type.GetConstructors()[0].Invoke(new object[] { this.ServiceProvider });
                    m_services[serviceType] = service;
                }
                else if (serviceType == typeof(IMonitorDocumentsService))
                {
                    DebugLog("Creating MonitorDocumentService");
                    service = new MonitorDocumentsService(this.ServiceProvider);
                    m_services[serviceType] = service;
                }
                else if (serviceType == typeof(INamedCommandService))
                {
                    DebugLog("Creating NamedCommandService");
                    service = new NamedCommandService(this.ServiceProvider, this.AddInInstance);
                    m_services[serviceType] = service;
                }
                else if (serviceType == typeof(IMetaFeatureService))
                {
                    service = this.Store;
                }
                else if (serviceType == typeof(IFeatureContext))
                {
                    service = m_globalContext;
                }
                else if (serviceType == typeof(IMenuCommandService))
                {
                    // INamedCommandService inherits from IMenuCommandService.
                    service = this.GetService<INamedCommandService>();
                }
                else if (serviceType == typeof(VsShell.IVsUIShell))
                {
                    service = this.ServiceProvider.GetService(typeof(VsShell.SVsUIShell));
                }
                else if (serviceType == typeof(VsShell.IVsRunningDocumentTable))
                {
                    service = this.ServiceProvider.GetService(typeof(VsShell.SVsRunningDocumentTable));
                }
                else if (serviceType == typeof(VsShell.IVsMonitorUserContext))
                {
                    service = this.ServiceProvider.GetService(typeof(VsShell.SVsMonitorUserContext));
                }
                else if (serviceType == typeof(VsShell.IVsCodeDefView))
                {
                    service = this.ServiceProvider.GetService(typeof(VsShell.SVsCodeDefView));
                }
                else
                {
                    service = this.ServiceProvider.GetService(serviceType);
                }
            }

            if (service == null)
                DebugLog("Failed to load service " + serviceType.FullName + ", null returned.");

            return service;
        }

        /// <summary>
        /// Gets service of specified type.
        /// </summary>
        /// <typeparam name="T">The type of the service to retreive</typeparam>
        /// <returns>Retrieved service object or null if not supported.</returns>
        [DebuggerStepThrough]
        public T GetService<T>() where T : class
        {
            return this.GetService(typeof(T)) as T;
        }

        #endregion
    }
}
